# tt_servlets.rb
# contains the webrick servlets
#
# Copyright (c) 2009 Alex Wilson
# Licensed under the MIT license (see LICENSE in the distribution root dir)

require "rubygems"
require "datafier"
require "optimiser"
require "webrick"
require "thread"
require "uuid"

require "json"
require "json/add/core"

module TTServer

# Holds the library mutex
class Lib
	@@lib_mutex = Mutex.new
	
	# Synchronize with the library mutex to execute a block
	def Lib.sync
		@@lib_mutex.synchronize do
			yield
		end
	end
end

# Manages tracking of background task threads
class Tasks
	@@thread_desc = Hash.new
	@@thread_status = Hash.new
	@@thread_mutex = Mutex.new
	
	# Register a background task thread
	def Tasks.register(thread, desc, status=:starting)
		@@thread_mutex.synchronize do
			@@thread_desc[thread]  = desc
			@@thread_status[thread] = status
		end
	end
	
	def Tasks.unregister(thread)
		@@thread_mutex.synchronize do
			@@thread_desc.delete(thread)
			@@thread_status.delete(thread)
		end
	end
	
	# Updates the status of a background task
	def Tasks.update(thread, status)
		@@thread_mutex.synchronize do
			@@thread_status[thread] = status
		end
	end
	
	# Gets a list of currently running Tasks
	def Tasks.all
		hash = Hash.new
		@@thread_mutex.synchronize do
			hash = (@@thread_status.keys.collect do |t|
				{ :thread_id => t.object_id, :desc => @@thread_desc[t], :status => @@thread_status[t] }
			end)
		end
		hash
	end
end

# Represents a user session with the application. Sessions by default are 
# temporary and will be lost if the server process restarts. If they are named, 
# however, they can persist in a disk store.
# 
# Sessions are referenced using their UUID, and Session.create/Session.get. They 
# contain an array of 'group keys' -- text references to groups in the timetable 
# to load when restoring the session.
class UserSession
  # The list of group keys that were saved in this session
	attr_accessor :group_keys
	
	# This session's UUID
	attr_reader :id
	# The username the session was saved under
	attr_accessor :user
	# A string description of the session
	attr_accessor :desc
	
	# true if the session is locked, false otherwise
	attr_accessor :locked
	# The key used to lock/unlock the session
	attr_accessor :lock_key
	
	# Stores the group keys of the session's optimal configuration
	# (used as cache)
	attr_accessor :optimal
	# Score of session's optimal configuration
	attr_accessor :optimal_score
	# Trace of session's optimap configuration
	attr_accessor :optimal_trace
	# Time taken to optimise this session
	attr_accessor :optimal_time
	
	@@sessions = Hash.new
	@@sessions_mutex = Mutex.new
	
	# Takes the UUID of the session
	# THIS SHOULD NOT BE CALLED. use create instead. :)
	def initialize(id)
		clear
		@id = id
		@user = nil
		@desc = ''
		@locked = false
		@optimal = nil
		@lock_key = ''
		@optimal_score = nil
	end
	
	# Adds a specified group key to the session
	def add_group(course, series, number)
		@group_keys << [course, series, number] if not @group_keys.include?([course,series,number])
	end
	
	# Removes a group key from the session
	def remove_group(course, series, number)
		@group_keys.delete( [course, series, number] )
	end
	
	# Removes all group keys from the session
	def clear
		@group_keys = Array.new
	end
	
	# Sets a name for the session to make it persistent
	def name(user, desc)
		@user = user
		@desc = desc
	end
	
	# Returns the group objects belonging to the session
	#
	# Shouldn't be touched unless you have Lib sync!
	def groups
		@group_keys.collect { |c,s,n| Timetable::Group.find(c,s,n) }
	end
	
	# Fetches a representation of the found optimal configuration in JSON format
	def optimal_to_json
		sess = Array.new
		Lib.sync do
			@optimal.each do |c,s,n| 
				g = Timetable::Group.find(c,s,n)
				sess << { :course => c, :series => s, :number => n, :events => g }
			end
		end
		sess.to_json	
	end
	
	# Fetches a representation of all the sessions' groups and their timetables in JSON format
	def to_json
		sess = Array.new
		Lib.sync do
			groups.each do |g|
				sess << { :course => g.course, :series => g.series, :number => g.number, :events => g }
			end
		end
		sess.to_json
	end
	
	# Fetches an iCal calendar containing all of the session's groups and their timetables
	def to_ical
		cal = Vpim::Icalendar.create2

		Lib.sync do
			@group_keys.each do |c,s,n|
				g = Timetable::Group.find(c,s,n)
				g.add_to_calendar(cal)
			end
		end
		
		cal.encode
	end
	
	# Creates a new user session and returns it
	def UserSession.create
		id = UUID.generate
		session = UserSession.new(id)
		@@sessions_mutex.synchronize do
			@@sessions[id] = session
		end
		session
	end
	
	# Saves all persistent sessions to disk
	def UserSession.save
		hash = Hash.new
		@@sessions_mutex.synchronize do
			@@sessions.each { |k,v|	hash[k] = v if (v.user) }
		end
		File.open("data/sessions.dat", "w") { |f| f.write Marshal.dump(hash) }
	end
	
	# Loads persistent sessions from disk
	def UserSession.load
		@@sessions_mutex.synchronize do
			File.open("data/sessions.dat") { |f| @@sessions = Marshal.load(f.read) } if File.file?("data/sessions.dat")
		end
	end
	
	# Gets the session by id if it is in memory
	def UserSession.get(id)
		session = nil
		@@sessions_mutex.synchronize do
			session = @@sessions[id]
		end
		session
	end
	
	# Gets a list of all persistent sessions
	def UserSession.all
		sessions = Array.new
		@@sessions_mutex.synchronize do
			sessions = @@sessions.values
		end
		sessions
	end
end

module Servlets

include TTServer
include WEBrick

# Control servlets can be used to stop/start the webrick server and alter
# its properties
#
# When created, the servlet is given an operation to perform. Whenever it
# receives a POST request, it then performs its assigned operation.
# 
# Operations
# - 'stop' -- Stops the server
class ControlServlet < HTTPServlet::AbstractServlet
	# op is the operation to perform
	def initialize(server, op)
		super(server)
		@op = op
	end
	
	def do_POST(req, resp)
		case @op
			when 'stop'
				resp['content-type'] = 'application/json'
				resp.body = { :status => 'stopping' }.to_json
				@server.stop
				raise HTTPStatus::OK
		end
	end
end

# Servlet for managing the loading of courses out of datafiles.
# This is kept for the purpose of loading datafiles created by an external process
# to the webserver, as in all other cases courses will be loaded by virtue of their creation.
#
# Receives a POST request with the 'course' parameter set to indicate
# the course code to load (if no course parameter set it does nothing)
#
# Whether or not course is set, it returns a list of all currently loaded 
# courses in JSON format.
#
# Usually mounted at /load
class LoaderServlet < HTTPServlet::AbstractServlet
	def do_POST(req, resp)
		resp['content-type'] = 'application/json'
		if req.query['course']
			course = req.query['course'].gsub(/[^A-Za-z0-9]/, '')
			
			Lib.sync do
				Timetable::Course.load course
				resp.body = Timetable::Course.all.collect { |c| c.course }.to_json
			end
			
			raise HTTPStatus::OK
		else
			Lib.sync do
				resp.body = Timetable::Course.all.collect { |c| c.course }.to_json
			end
			raise HTTPStatus::OK
		end
	end
end

# Takes a GET request with no parameters and spits back a JSON array
# of available courses and their update timestamps.
#
# Usually mounted at /loadable
#
# Format:
#   [
#     { course: 'COUR1234', updated: 'dd/mm/yy' },
#     { course: 'COUR1235', updated: 'dd/mm/yy' } 
#   ]
class LoadableServlet < HTTPServlet::AbstractServlet
	def do_GET(req, resp)
		resp['content-type'] = 'application/json'
		resp['cache-control'] = 'no-cache'
		Lib.sync do
			resp.body = Timetable::Course.get_tstamps.sort.collect { |c,ts| 
				{ :course => c, :updated => ts.strftime("%d/%m %H:%M") } 
			}.to_json
		end
		
		raise HTTPStatus::OK
	end
end

# Servlet for initiating and managing course fetch operations.
# It takes a POST request with optional parameter 'course' --
# if specified it starts to refresh this course from the CourseFactory.
# Whether specified or not, it returns a list of running Tasks in JSON
# format.
# 
# Usually mounted at /fetch
#
# Example format:
#   [
#     { thread_id: 1234, 
#       desc: 'description string',
#       status: 'status string' }, ...
#   ]
class FetcherServlet < HTTPServlet::AbstractServlet

	# Starts a fetcher thread for the given course
	def FetcherServlet.start_fetcher(course)
		course = course.upcase
		t = Thread.new do
			begin
				Tasks.register(Thread.current, "update #{course}", :preparing)

				f = Timetable::CourseFactory.new([course])
				
				Tasks.update(Thread.current, :fetching)
				f.fetch
				
				Tasks.update(Thread.current, :waiting)
				
				Lib.sync do
				
					Tasks.update(Thread.current, :parsing)
					
					c = Timetable::Course.find_if(course)
					c.unregister if c
					f.parse
					
				end
				Tasks.update(Thread.current, :waiting)
				Lib.sync do
					Tasks.update(Thread.current, :dumping)
					Timetable::Course.find(course).dump
				end
				Tasks.update(Thread.current, :done)
				sleep 5
				Tasks.unregister(Thread.current)
			rescue => err
				Tasks.update(Thread.current, "error: #{err}")
				p "FINDME:"+err
				p ["FINDME:"]+err.backtrace
				sleep 10
				Tasks.unregister(Thread.current)
			end
		end
	end
	
	def do_POST(req, resp)
		if req.query['course']
		
			FetcherServlet.start_fetcher(req.query['course'])
		
			resp['content-type'] = 'application/json'
			resp.body = { :status => 'started' }.to_json
			raise HTTPStatus::OK
		else
			resp['content-type'] = 'application/json'
			resp.body = Tasks.all.to_json
		end
	end
end

# An abstract JSON servlet.
class AbstractJSONServlet < HTTPServlet::AbstractServlet
  # Start a JSON request. This sets the Content-Type, Cache-Control
  # headers automatically for JSON output.
	def json(req, resp, check_params=[])
		resp['content-type'] = 'application/json'
		resp['cache-control'] = 'no-cache'
		check_params.each do |p|
			if not req.query[p]
				raise HTTPStatus::PreconditionFailed
				return
			end
		end
		raise yield
	end
end

# Servlet for fetching a JSON representation of a specified group timetable.
class JSONTimetableServlet < AbstractJSONServlet
	def do_GET(req, resp)
		json req, resp, ['course', 'series', 'number'] do
			Lib.sync do
				g = Timetable::Group.find(req.query['course'], req.query['series'], req.query['number'])
				resp.body = g.to_json
			end
			
			HTTPStatus::OK
		end
	end
end

# Servlet for fetching the groups belonging to a course
class JSONGroupsServlet < AbstractJSONServlet
	def do_GET(req, resp)
		json req, resp, ['course'] do
			Lib.sync do
				c = Timetable::Course.find_if(req.query['course'])
				resp.body = (c.series.sort.collect do |k,s|
					{ :series => k, :numbers => s.groups.keys.sort { |x,y| x.to_i <=> y.to_i } }
				end).to_json
			end
			HTTPStatus::OK
		end
	end
end

# begins a global update
class GlobalUpdateServlet < AbstractJSONServlet
	def do_POST(req, resp)
		json req, resp do
			t = Thread.new do
				begin
					Tasks.register(Thread.current, "global update", :running)
					ca = []
					Lib.sync do
						ca = Timetable::Course.get_index
					end
					ca.each do |c|
						FetcherServlet.start_fetcher(c)
						sleep 60
					end
					Tasks.update(Thread.current, :done)
					sleep 5
					Tasks.unregister(Thread.current)
				rescue StandardError => err
					Tasks.update(Thread.current, :error)
				end
			end
			
			resp.body = { :status => :started, :thread_id => t.object_id }.to_json
			HTTPStatus::OK
		end
	end
end

# Abstract class for session servlets
class AbstractSessionServlet < HTTPServlet::AbstractServlet
	def session(req, resp, needs_session=true, content_type='application/json')
		if needs_session and not req.query['session']
			raise HTTPStatus::PreconditionFailed
			return
		end
		session = UserSession.get(req.query['session'])
		resp['content-type'] = content_type
		resp['cache-control'] = 'no-cache'
		ex = yield session
		UserSession.save
		raise ex
	end
end

# Servlet to create a new user session and return it.
class NewSessionServlet < AbstractSessionServlet
	def do_POST(req, resp)
		session(req, resp, false) do |session|
			session = UserSession.create
			resp.body = { :session => session.id }.to_json
			HTTPStatus::OK
		end
	end
end

# Servlet to add a group entry to a session
class AddToSessionServlet < AbstractSessionServlet
	def do_POST(req, resp)
		session(req, resp) do |session|
			session.add_group(req.query['course'], req.query['series'], req.query['number']) if not session.locked
			
			Lib.sync do
				g = Timetable::Group.find(req.query['course'], req.query['series'], req.query['number'])
				resp.body = g.to_json
			end
			HTTPStatus::OK
		end
	end
end

# Servlet for clearing the session of all groups
class ClearSessionServlet < AbstractSessionServlet
	def do_POST(req, resp)
		session(req, resp) do |session|
		  if session.locked
		    resp.body = { :status => :locked }.to_json
		  else
			  session.clear
			  resp.body = { :status => :ok }.to_json
		  end
			HTTPStatus::OK
		end
	end
end

# Servlet for removing groups from the session
class RemoveFromSessionServlet < AbstractSessionServlet
	def do_POST(req, resp)
		session(req, resp) do |session|
		  if session.locked
		    resp.body = { :status => :locked }.to_json
		  else
			  session.remove_group(req.query['course'], req.query['series'], req.query['number'])
			  resp.body = { :status => :ok }.to_json
			end
			HTTPStatus::OK
		end
	end
end

# Servlet for retrieving data about the optimisation system
class OptimisationOptionsServlet < HTTPServlet::AbstractServlet
  def do_GET(req, resp)
    penalties =   Timetable::Optimisation::Option::PENALTIES
    descs =       Timetable::Optimisation::Option::PENALTY_DESCS
    defaults =    Timetable::Optimisation::Option::PENALTY_DEFAULTS
    
    options = []
    (0..penalties.size-1).each do |i|
      options << { :name => penalties[i], :desc => descs[i], :normal => defaults[penalties[i]]}
    end
    
    resp['content-type'] = 'application/json'
		resp['cache-control'] = 'no-cache'
    resp.body = options.to_json
    raise HTTPStatus::OK
  end
end

# Servlet for handling timetable optimisation and retrieval of results
class OptimiseSessionServlet < AbstractSessionServlet
	def do_POST(req, resp)
		session(req, resp) do |session|
		  config = JSON.parse(req.query['config'])
		  puts "FINDME: #{config.inspect}"
			session.optimal = nil
			resp.body = { :status => :running }.to_json
			t = Thread.new do
				begin
					Tasks.register(Thread.current, "optimiser", :starting)
					start_time = Time.now
					breeder = nil
					#Lib.sync do
						series = session.groups.collect { |g| Timetable::Series.find(g.course, g.series) }.uniq
						breeder = Timetable::Optimisation::Optimiser.new(series, config)
						Tasks.update(Thread.current, :running)
						breeder.run do |cnt, best|
							Tasks.update(Thread.current, "running #{cnt} (#{best})")
						end
					#end
					Tasks.update(Thread.current, :saving)
					session.optimal = breeder.best.groups.collect { |g| [g.course, g.series, g.number] }
					session.optimal_trace = breeder.best_trace
					session.optimal_score = breeder.best_score
					end_time = Time.now
					session.optimal_time = end_time - start_time
					UserSession.save
					Tasks.update(Thread.current, :done)
					sleep 5
					Tasks.unregister(Thread.current)
				rescue => err
					puts "FINDME: #{err}"
					p ['FINDME:']+err.backtrace
					session.optimal = :error
					Tasks.update(Thread.current, :error)
					sleep 10
					Tasks.unregister(Thread.current)
				end
			end
			HTTPStatus::OK
		end
	end
	
	def do_GET(req, resp)
		session(req, resp) do |session|
			if session.optimal
			  if session.optimal == :error then
			    resp.body = {:status => :error }.to_json
			  else
				  # can't use to_json as we're overriding the method
  				# todo: fix this up a bit
  				t = session.optimal_time
  				if ((t / 60).truncate / 60).truncate > 0
  				  timestr = "#{(t / 60 / 60).truncate} hr #{(t / 60 % 60).truncate} min #{(t % 60 % 60).truncate} s"
  				else
  				  timestr = "#{(t / 60).truncate} min #{(t % 60).truncate} s"
  				end
  				resp.body = "{ status: 'done', time: '#{timestr}', trace: #{session.optimal_trace.to_json}, fitness: #{session.optimal_score}, session: #{session.optimal_to_json} }"
  				#session.optimal = nil # only give one response.. bug!
  			end
			else
				resp.body = { :status => :running }.to_json
			end
			HTTPStatus::OK
		end
	end
end

# Servlet to name the session to make it persistent
class NameSessionServlet < AbstractSessionServlet
	def do_POST(req, resp)
		session(req, resp) do |session|
		  if session.locked
		    resp.body = { :status => :locked }.to_json
		  else
			  session.name(req.query['user'], req.query['desc'])
			  resp.body = { :status => :ok }.to_json
		  end
			HTTPStatus::OK
		end
	end
end

# Servlet to lock the session
class LockSessionServlet < AbstractSessionServlet
	def do_POST(req,resp)
		session(req, resp) do |session|
		  if session.locked
		    resp.body = { :status => :failure }
		  else
			  session.lock_key = req.query['key']
			  session.locked = true
			  resp.body = { :status => :locked }.to_json
		  end
			HTTPStatus::OK
		end
	end
	
	def do_GET(req, resp)
		session(req, resp, false) do |session|
			resp.body = { :status => { true => :locked, false => :unlocked }[session.locked] }.to_json
			HTTPStatus::OK
		end
	end
end

# Servlet to unlock the session (or attempt to)
class UnlockSessionServlet < AbstractSessionServlet
	def do_POST(req, resp)
		session(req, resp) do |session|
			session.locked = false if req.query['key'] == session.lock_key
			resp.body = { :status => { true => :locked, false => :unlocked }[session.locked] }.to_json
			HTTPStatus::OK
		end
	end
end

# Servlet to list all available persistent sessions
class ListSessionServlet < AbstractSessionServlet
	def do_GET(req, resp)
		session(req, resp, false) do |session|
			resp.body = (UserSession.all.collect do |s|
				{ :id => s.id, :user => s.user, :desc => s.desc } if s.user
			end).compact.to_json
			HTTPStatus::OK
		end
	end
end

# Returns the session's events in JSON format
class JSONSessionServlet < AbstractSessionServlet
	def do_GET(req, resp)
		session(req, resp) do |session|
			resp.body = session.to_json
			HTTPStatus::OK
		end
	end
end

# Returns the session's events in iCal format
class ICalSessionServlet < AbstractSessionServlet
	def do_GET(req, resp)
		session(req, resp, true, 'text/calendar') do |session|
			resp.body = session.to_ical
			HTTPStatus::OK
		end
	end
end

end

end
